El análisis exploratorio de datos (EAD, por sus siglas en inglés) es una fase crucial en el proceso de modelado de series temporales, ya que nos permite entender las características subyacentes de los datos antes de aplicar cualquier modelo predictivo. En este notebook, realizaremos un análisis exhaustivo de los factores que podrían influir en el IBEX-35, utilizando diferentes métodos estadísticos y técnicas de selección de características. Posteriormente, nos adentraremos en un análisis detallado de cada serie, identificando patrones, tendencias y posibles irregularidades que podrían afectar el comportamiento de los datos.
Apartados a tratar
Métodos para seleccionar los factores más relevantes
Análisis de las series temporales
Otras pruebas estadísticas
import pandas as pd
from sklearn.decomposition import PCA
from sklearn.preprocessing import StandardScaler
import matplotlib.pyplot as plt
import matplotlib.dates as mdates
import statsmodels.api as sm
import numpy as np
from statsmodels.tsa.seasonal import seasonal_decompose
from IPython.display import Image, Markdown, display_html
from statsmodels.tsa.stattools import adfuller
from sklearn.ensemble import IsolationForest
from matplotlib import gridspec
from river import drift
import seaborn as sns
import math
from sklearn.ensemble import RandomForestRegressor
from sklearn.feature_selection import RFE
from statsmodels.tsa.stattools import ccf
from matplotlib import gridspec
df = pd.read_csv('datos/full_df.csv')
df.head(5)
| fecha | ibex_close | ibex_volume | deuda_perc_pib | gasto_perc_PIB | pib_trim_per_capita | var_pib | var_anual_pib | tasa_desempleo | tie | ipc | prima | eurusd_close | tim | |
|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
| 0 | 1994-01-03 | 3654.500000 | 0 | NaN | 44.83 | 2.7 | 0.0 | -6.9 | 22.2 | NaN | 1.01 | 248.0 | NaN | NaN |
| 1 | 1994-01-04 | 3630.300049 | 0 | NaN | 44.83 | 2.7 | 0.0 | -6.9 | 22.2 | NaN | 1.01 | 252.0 | NaN | NaN |
| 2 | 1994-01-05 | 3621.199951 | 0 | NaN | 44.83 | 2.7 | 0.0 | -6.9 | 22.2 | NaN | 1.01 | 256.0 | NaN | NaN |
| 3 | 1994-01-07 | 3636.399902 | 0 | NaN | 44.83 | 2.7 | 0.0 | -6.9 | 22.2 | NaN | 1.01 | 258.0 | NaN | NaN |
| 4 | 1994-01-10 | 3660.600098 | 0 | NaN | 44.83 | 2.7 | 0.0 | -6.9 | 22.2 | NaN | 1.01 | 255.0 | NaN | NaN |
En este apartado seleccionaremos las mejores variables para nuestro modelo a partir de los resultados de RFR, RFE, PCA, Correlación y Correlación cruzada.
El modelo RandomForestRegressor es un algoritmo de aprendizaje supervisado que está construido en árboles de decisión. De esta forma, el modelo promedia las predicciones de múltiples árboles individuales y mide cuantas veces una variable es seleccionada para dividir los nodos en todos los árboles de decisión. Además, se calcula automáticamente una métrica de importancia, en este caso MSE para cada variable. La librería que se ha usado para el RFR es Sklearn.
$\hat{y} = \frac{1}{N} \sum_{i=1}^{N} f_i(X)$
dónde:
dfML = df.copy()
dfML = dfML.rename(columns={'ibex_close': 'y'})
dfML = dfML.rename(columns={'fecha': 'ds'})
dfML.columns
Index(['ds', 'y', 'ibex_volume', 'deuda_perc_pib', 'gasto_perc_PIB',
'pib_trim_per_capita', 'var_pib', 'var_anual_pib', 'tasa_desempleo',
'tie', 'ipc', 'prima', 'eurusd_close', 'tim'],
dtype='object')
full_dfml = dfML[["ibex_volume", "pib_trim_per_capita", "ipc", "prima", "gasto_perc_PIB", "tasa_desempleo", "ds", "y"]]
short_dfml = dfML[["ibex_volume", "pib_trim_per_capita", "ipc", "prima", "gasto_perc_PIB", "tasa_desempleo","deuda_perc_pib",
"tie", "eurusd_close", "ds", "y"]].dropna()
Comprobamos los datos más relevantes con el conjunto de datos formado con las series que cumplen todo el rango de datos que tiene el IBEX-35.
bestFeatures = full_dfml.drop({'ds'}, axis=1)
array = bestFeatures.values
X = array[:,1:]
y = array[:,0]
model = RandomForestRegressor(n_estimators=500, random_state=1)
model.fit(X, y)
importance_scores = model.feature_importances_
sorted_idx = importance_scores.argsort()[::-1]
sorted_scores = importance_scores[sorted_idx]
names = bestFeatures.columns.values[1:]
sorted_names = names[sorted_idx]
ticks = [i for i in range(len(sorted_names))]
plt.bar(ticks, sorted_scores)
plt.xticks(ticks, sorted_names, rotation=45, fontsize = 7, ha = 'right')
plt.title("Prioridad de las variables según RandomForestRegressor", fontsize=13)
plt.style.use('fivethirtyeight')
plt.show()
Observaciones
Podemos ver como la Prima, el PIB trimestral per cápita son los factores más relevantes.
Comprobamos los datos más relevantes con el conjunto de datos formado con las series que no cumplen todo el rango de datos que tiene el IBEX-35.
bestFeatures = short_dfml.drop({'ds'}, axis=1)
array = bestFeatures.values
X = array[:,1:]
y = array[:,0]
model = RandomForestRegressor(n_estimators=500, random_state=1)
model.fit(X, y)
importance_scores = model.feature_importances_
sorted_idx = importance_scores.argsort()[::-1]
sorted_scores = importance_scores[sorted_idx]
names = bestFeatures.columns.values[1:]
sorted_names = names[sorted_idx]
ticks = [i for i in range(len(sorted_names))]
plt.bar(ticks, sorted_scores)
plt.xticks(ticks, sorted_names, rotation=45, fontsize = 7, ha = 'right')
plt.title("Prioridad de las variables según RandomForestRegressor", fontsize=13)
plt.style.use('fivethirtyeight')
plt.show()
Observaciones
Vemos que cuando añadimos el resto de factores la deuda cobra importancia.
El Recursive Feature Elimination (RFE) es otra técnica de selección de variables el cual itera por los diferentes modelos que va creando, cada uno más simple y eficiente, seleccionando las variables más importantes. En este caso RFE usa el modelo RFR y evalúa su precisión. Al igual que RFR, se ha usado RFE de la librería Sklearn.
Empezamos con los datos que tienen el rango de fechas completo.
bestFeatures = full_dfml.drop({'ds'}, axis=1)
array = bestFeatures.values
X = array[:,1:]
y = array[:,0]
rfe = RFE(RandomForestRegressor(n_estimators=500, random_state=1), n_features_to_select=4)
fit = rfe.fit(X, y)
print('Selected Features:')
names = bestFeatures.columns.values[1:]
for i in range(len(fit.support_)):
if fit.support_[i]:
print(names[i])
names = bestFeatures.columns.values[1:]
sorted_idx = fit.ranking_.argsort()
sorted_names = names[sorted_idx]
sorted_rank = fit.ranking_[sorted_idx]
ticks = [i for i in range(len(sorted_names))]
plt.bar(ticks, sorted_rank)
plt.xticks(ticks, sorted_names, rotation=45, fontsize = 7, ha = 'right')
plt.title("Prioridad de las variables según RFE", fontsize=13)
plt.show()
Selected Features: pib_trim_per_capita prima tasa_desempleo y
Observaciones
Podemos ver como el PIB trimestral per cápita, la Prima de riesgo y la tasa de empleo son los factores más importantes.
bestFeatures = short_dfml.drop({'ds'}, axis=1)
array = bestFeatures.values
X = array[:,1:]
y = array[:,0]
rfe = RFE(RandomForestRegressor(n_estimators=500, random_state=1), n_features_to_select=4)
fit = rfe.fit(X, y)
print('Selected Features:')
names = bestFeatures.columns.values[1:]
for i in range(len(fit.support_)):
if fit.support_[i]:
print(names[i])
names = bestFeatures.columns.values[1:]
sorted_idx = fit.ranking_.argsort()
sorted_names = names[sorted_idx]
sorted_rank = fit.ranking_[sorted_idx]
ticks = [i for i in range(len(sorted_names))]
plt.bar(ticks, sorted_rank)
plt.xticks(ticks, sorted_names, rotation=45, fontsize = 7, ha = 'right')
plt.title("Prioridad de las variables según RFE", fontsize=13)
plt.show()
Selected Features: pib_trim_per_capita deuda_perc_pib eurusd_close y
Observaciones
Si seleccionamos todas las variables podemos ver como el PIB trimestral per cápita, la deuda y el Eurusd son los más relevantes.
El Análisis de Componentes Principales (PCA, por sus siglas en inglés) es una técnica comúnmente utilizada para la reducción de la dimensionalidad de un conjunto de datos grande. En este caso, PCA puede servir para ver qué variables tienen una contribución mayor en los diferentes componentes. Es decir, cuanta información explica esa variable en cada uno de los componentes. Así pues, para trabajar con PCA, lo primero que hay que hacer es estandarizar nuestra matriz de características $X$ que será proyectada sobre los componentes principales $W$ , obteniendo así las nuevas coordenadas del conjunto de datos:
$Z=X*W$
Donde: $X$: Es la matriz de datos estandarizados $W$: Es la matriz de vectores propios $Z$: Es la matriz transformada que contiene los componentes principales
Una vez se ha obtenido la matriz resultante, se pueden observar las cargas de las variables originales en los componentes $w_{ij}$. Esta contribución se interpreta como la magnitud de carga en el componente. Cuanto mayor la carga, mayor será la contribución. PCA se ha usado de la librería Sklearn.
X = df.drop(columns=['fecha', 'ibex_close', 'deuda_perc_pib', 'tie', 'eurusd_close', 'tim'])
y = df['ibex_close']
scaler = StandardScaler()
X_scaled = scaler.fit_transform(X)
pca = PCA(n_components=X.shape[1])
X_pca = pca.fit_transform(X_scaled)
varianza_explicada = pca.explained_variance_ratio_
varianza_acumulada = varianza_explicada.cumsum()
varianza_acumulada
array([0.37873453, 0.56888802, 0.71373018, 0.83827354, 0.90426445,
0.95445829, 0.9822229 , 1. ])
Vemos que a partir del tercer componente ya explicamos un poco más del 70% de la varianza.
plt.figure(figsize=(8, 6))
plt.plot(range(1, len(varianza_acumulada) + 1), varianza_acumulada, marker='o', linestyle='--')
plt.xlabel('Número de Componentes Principales')
plt.ylabel('Varianza Explicada Acumulada')
plt.title('Varianza Explicada Acumulada por los Componentes Principales')
plt.show()
cargas = pd.DataFrame(pca.components_, columns=X.columns, index=[f'PC{i+1}' for i in range(len(varianza_explicada))])
cargas_absolutas = cargas.abs()
contribucion_total = cargas_absolutas.sum(axis=0)
ranking_contribucion = contribucion_total.sort_values(ascending=False)
ranking_contribucion
prima 2.557131 ibex_volume 2.484245 gasto_perc_PIB 2.421650 tasa_desempleo 2.272416 var_anual_pib 2.268922 pib_trim_per_capita 2.207634 var_pib 2.073457 ipc 1.666815 dtype: float64
Observaciones
Vemos como la Prima de riesgo, el volúmen del IBEX, el gasto del PIB y la tasa de desempleo son los factores que más contribuyen.
df['fecha'] = pd.to_datetime(df['fecha'])
df_filtrado = df[df['fecha'] >= '2010-01-12']
X = df_filtrado.drop(columns=['fecha', 'ibex_close'])
y = df['ibex_close']
scaler = StandardScaler()
X_scaled = scaler.fit_transform(X)
pca = PCA(n_components=X.shape[1])
X_pca = pca.fit_transform(X_scaled)
varianza_explicada = pca.explained_variance_ratio_
varianza_acumulada = varianza_explicada.cumsum()
varianza_acumulada
array([0.33646133, 0.53325198, 0.63538359, 0.72437424, 0.81190068,
0.88643497, 0.92717089, 0.9577301 , 0.98274389, 0.99504486,
0.99856275, 1. ])
Si usamos todas las variables, no es hasta el cuarto componente que explicamos un poco más del 70% de la varianza.
plt.figure(figsize=(8, 6))
plt.plot(range(1, len(varianza_acumulada) + 1), varianza_acumulada, marker='o', linestyle='--')
plt.xlabel('Número de Componentes Principales')
plt.ylabel('Varianza Explicada Acumulada')
plt.title('Varianza Explicada Acumulada por los Componentes Principales')
plt.show()
cargas = pd.DataFrame(pca.components_, columns=X.columns, index=[f'PC{i+1}' for i in range(len(varianza_explicada))])
cargas_absolutas = cargas.abs()
contribucion_total = cargas_absolutas.sum(axis=0)
ranking_contribucion = contribucion_total.sort_values(ascending=False)
ranking_contribucion
gasto_perc_PIB 2.971683 prima 2.887746 var_anual_pib 2.851320 tasa_desempleo 2.823183 deuda_perc_pib 2.797426 ibex_volume 2.743999 tie 2.704832 var_pib 2.644765 eurusd_close 2.577356 tim 2.382572 pib_trim_per_capita 2.285732 ipc 2.154885 dtype: float64
En este caso, el gasto del PIB, la Prima, la varianza anual del PIB, la Tasa de desempleo, la deuda, el volumne del IBEX y los Tipos de interés són los que más contribuyen.
La correlación es una medida estadística que describe el grado y la dirección de la relación lineal entre dos variables. Es decir, mide cómo de bien se alinean las variaciones de una variable con las variaciones de otra. Si dos variables aumentan o disminuyen juntas, su correlación será positiva. Si una variable aumenta mientras la otra disminuye, la correlación será negativa.
$ r = \frac{\sum (X_i - \overline{X})(Y_i - \overline{Y})}{\sqrt{\sum (X_i - \overline{X})^2 \sum (Y_i - \overline{Y})^2}} $
Donde:
plt.figure(figsize=(12, 10))
corr_matrix = df.drop(columns=["fecha"]).corr()
sns.heatmap(corr_matrix, annot=True, cmap="coolwarm")
plt.show()
Observaciones
Los resultados de la correlación de Pearson nos muestra como el PIB trimestral per cápita, la tasa de desempleo, la Prima de riesgo y el Eurusd son los más correlacionados con el cierre del IBEX (ibex_close).
Finalmente, se decide escoger la Deuda, el PIB trimestral por cápita, la Prima de riesgo y la tasa de desempleo como factores exógenos que analizaremos y usaremos para la predicción del IBEX-35.
df = df[["ibex_close", "deuda_perc_pib", "pib_trim_per_capita", "prima", "tasa_desempleo", "fecha"]]
Por último, se ha realizado una correlación cruzada para ver la relación entre la variable dependiente ‘ibex_close’ y los demás factores exógenos. En este caso, el propósito es identificar cómo se afectan mutuamente las series en diferentes lags. Para usar la correlación cruzada se ha usado la librería Statsmodels.
$ R_{XY}(\tau) = \sum_{t} X(t) \cdot Y(t + \tau) $
Donde:
El resultado que nos proporciona esta librería son unos correlogramas donde el eje X representan los lags. Es decir, indican el desfase temporal entre las series. Mientras que el eje Y muestra los coeficientes de correlación cruzada PXY(k) que pueden tomar valores entre -1 y 1. Así pues, en la gráfica se pueden ver picos positivos, que significan que hay lags en que las dos series aumentan o disminuyen juntas; picos negativos, que indican que hay lags en los que una serie aumenta mientras la otra disminuye; y por último, si vemos que la línea va muy junta del valor 0 del eje Y significa que no hay una correlación muy fuerte en ese lag.
df_ccf = df.set_index('fecha')
exogenas = [col for col in df_ccf.columns if col != 'ibex_close']
n_cols = 2
n_rows = math.ceil(len(exogenas) / n_cols)
fig, axes = plt.subplots(n_rows, n_cols, figsize=(12, n_rows * 4))
axes = axes.flatten()
for i, exogena in enumerate(exogenas):
combined = df_ccf[['ibex_close', exogena]].dropna()
correlacion_cruzada = ccf(combined['ibex_close'], combined[exogena], adjusted=False)
axes[i].plot(correlacion_cruzada)
axes[i].set_title(f"ibex_close vs {exogena} ({combined.index.min()} - {combined.index.max()})", fontsize=12)
axes[i].set_xlabel("Lags", fontsize=10)
axes[i].set_ylabel("Correlación", fontsize=10)
axes[i].grid(True)
for j in range(len(exogenas), len(axes)):
fig.delaxes(axes[j])
plt.tight_layout()
plt.show()
Observaciones
Deuda pública: En el correlograma se observa que en los primeros 4000 lags, la correlación es negativa, alcanzando un valor de -0.175. Esto indica que, en ese intervalo de desfase temporal, el IBEX-35 tiende a bajar a medida que la deuda aumenta. Sin embargo, después de los primeros 4000 lags, la correlación se vuelve muy pequeña y no significativa, sugiriendo que la relación entre el IBEX-35 y la deuda pierde relevancia a medida que el desfase temporal aumenta. Este análisis sugiere que la influencia de la deuda sobre el IBEX-35 es transitoria y que la relación pierde fuerza a medida que se extiende el tiempo.
PIB trimestral por cápita: PIB trimestral por cápita: Se observa que en los primeros lags hay una correlación positiva bastante fuerte, cercana a 0.50, lo que indica que el PIB trimestral per cápita y el IBEX-35 tienden a subir juntos en ese intervalo. A medida que aumentan los lags, la correlación disminuye progresivamente hasta acercarse a 0 en el lag 1000. Posteriormente, se observa un pico negativo alrededor del lag 3000, sugiriendo que, en ese desfase temporal, el aumento del PIB trimestral por cápita podría estar asociado con una caída en el IBEX-35. No es hasta alrededor del lag 4000 cuando la relación se estabiliza cerca de 0, lo que sugiere que, a largo plazo, la relación entre el PIB per cápita y el IBEX-35 se debilita o se vuelve insignificante. Este análisis sugiere que el PIB trimestral por cápita tiene una relación positiva con el IBEX-35 a corto plazo, pero esta relación cambia a medida que el desfase temporal se alarga.
df['fecha'] = pd.to_datetime(df['fecha'])
numeric_columns = df.select_dtypes(include='number').columns
for col in numeric_columns:
current_factor = df[['fecha', col]].dropna()
fig, ax = plt.subplots(figsize=(25, 5))
ax.plot(current_factor['fecha'], current_factor[col], linewidth=2, color='blue')
ax.set_xlabel('Fecha')
ax.set_ylabel(col)
ax.set_title(f'{col} a lo largo del tiempo ({current_factor.fecha.dt.year.min()} - {current_factor.fecha.dt.year.max()})', fontsize=16)
xtick_labels = []
xticks = []
years_processed = set()
for i, idx in enumerate(current_factor['fecha']):
year = idx.year
if idx.month == 1 and year not in years_processed:
xtick_labels.append(str(year))
ax.axvline(x=idx.replace(month=1, day=1), color='black', linestyle='solid', linewidth=0.1)
xticks.append(i)
if idx.year % 2 == 0:
start_of_year = idx.replace(month=1, day=1)
ax.axvspan(start_of_year, start_of_year.replace(year=year + 1), color='grey', alpha=0.1)
years_processed.add(year)
elif idx.month == 7:
xtick_labels.append('')
ax.axvline(x=idx.replace(month=7, day=1), color='red', linestyle='-', linewidth=0.05)
xticks.append(i)
else:
xtick_labels.append('')
xticks.append(i)
ax.xaxis.set_major_locator(mdates.YearLocator(2))
plt.show()
Observaciones
Para analizar la variabilidad se ha calculado el coeficiente de variación a partir de la media y la desviación estándar. “El CV es una medida que nos dice cómo de agrupado es un conjunto de datos” (Sujit, S., 2015).
CV = $(σ / μ) * 100$
Donde:
σ: Mide la dispersión de los datos respecto a la media. Indica la cantidad de variabilidad en las observaciones.
μ: Es el valor medio de la serie temporal.
Un valor de CV más elevado nos dirá que la serie tiene bastante variabilidad. En nuestro caso, cómo estamos trabajando con factores económicos, la volatilidad es muy presente, por lo que los resultados pueden ser bastante altos.
df['fecha'] = pd.to_datetime(df['fecha'])
numeric_columns = df.select_dtypes(include='number').columns
count = 0
for col in numeric_columns:
count += 1
current_factor = df[['fecha', col]].dropna()
mean_factor = np.mean(current_factor[col])
std_factor = np.std(current_factor[col])
cv = round((std_factor / mean_factor) * 100, 2)
fig, ax = plt.subplots(figsize=(25, 5))
ax.errorbar(current_factor.fecha, current_factor[col], yerr=std_factor, fmt='o', ecolor='orange', capsize=3)
ax.axhline(mean_factor, color='red', linestyle='--', label='Mitjana')
ax.set_title(f'Variabilidad de {col}\n El coeficiente de variación es: {cv}')
ax.set_xlabel('Fecha')
ax.set_ylabel(col)
xtick_labels = []
xticks = []
years_processed = set()
for i, idx in enumerate(current_factor['fecha']):
year = idx.year
if idx.month == 1 and year not in years_processed:
xtick_labels.append(str(year))
ax.axvline(x=idx.replace(month=1, day=1), color='black', linestyle='solid', linewidth=0.1)
xticks.append(i)
if idx.year % 2 == 0:
start_of_year = idx.replace(month=1, day=1)
ax.axvspan(start_of_year, start_of_year.replace(year=year + 1), color='grey', alpha=0.1)
years_processed.add(year)
elif idx.month == 7:
xtick_labels.append('')
ax.axvline(x=idx.replace(month=7, day=1), color='red', linestyle='-', linewidth=0.05)
xticks.append(i)
else:
xtick_labels.append('')
xticks.append(i)
ax.xaxis.set_major_locator(mdates.YearLocator(2))
Observaciones
Cierre del IBEX-35 (28.3): El coeficiente de variación de 28.3 indica que el IBEX-35 presenta una variabilidad considerable, lo que es coherente con su naturaleza de índice bursátil, que tiende a experimentar fluctuaciones diarias y reacciones a eventos económicos, políticos y financieros. Un valor de 28.3 es relativamente alto, lo que muestra que los precios de cierre del IBEX-35 tienen una alta volatilidad en relación con su media.
Deuda pública (36.31): La deuda pública muestra un CV de 36.31, lo cual también indica una variabilidad considerable. Aunque la deuda pública tiende a ser más estable que los índices bursátiles, las crisis financieras, los cambios en las políticas fiscales y las fluctuaciones en la economía pueden generar variaciones notables en el nivel de la deuda. Este valor alto refleja cómo la deuda pública puede ser sensible a situaciones excepcionales, como crisis económicas o políticas expansivas.
PIB trimestral por cápita (25.03): Un CV de 25.03 para el PIB trimestral per cápita también señala una variabilidad moderada. Aunque el PIB tiende a seguir una tendencia de crecimiento a largo plazo, los ciclos económicos y las recesiones pueden generar fluctuaciones más o menos pronunciadas, como se vio en la crisis financiera de 2008 o la recesión causada por la pandemia de COVID-19. Este valor refleja la capacidad del PIB para fluctuar dependiendo de las condiciones macroeconómicas.
Prima de riesgo (106.12): La prima de riesgo presenta el valor más alto del conjunto con un CV de 106.12. Esto es indicativo de una alta volatilidad, lo cual es consistente con el comportamiento de la prima de riesgo. La prima de riesgo refleja la diferencia entre los bonos del gobierno español y los bonos alemanes, y está fuertemente influenciada por la percepción del riesgo de la economía española. Momentos de crisis o inestabilidad política pueden generar picos muy pronunciados en la prima de riesgo, lo que resulta en un alto coeficiente de variación.
Tasa de desempleo (31.69): Finalmente, la tasa de desempleo tiene un CV de 31.69. Esto indica que, si bien la tasa de desempleo tiende a seguir una trayectoria relativamente estable, la crisis económica y otros factores como los cambios en las políticas laborales o los efectos de la pandemia pueden generar fluctuaciones significativas. Un CV de 31.69 sugiere que hay una variabilidad considerable, especialmente en periodos de crisis económica.
La tendencia no es nada más que la dirección general y el patrón de crecimiento o disminución de los valores a lo largo del tiempo y que se puede entender como:
tendencia = $currentFactor_{t-1}$ + $currentFactor_t$
dónde:
Así pues, la tendencia nos permite ver la evolución general de los datos. En caso de que la tendencia sea creciente, significará que los valores medios están aumentando a lo largo del tiempo. I en caso contrario si la tendencia es a la baja. En cambio, si la serie presenta una tendencia horizontal, querrá decir que los valores medios se mantienen constantes en el tiempo. En este trabajo se ha analizado la tendencia y se ha establecido un nivel de significancia del 0.05 para aceptar la hipótesis nula en caso de que el resultado sea menor a este valor de p. Es decir, si el valor de p < 0.05 significará que hay suficiente confianza para concretar que existe tendencia en la serie.
df['fecha'] = pd.to_datetime(df['fecha'])
columns = df.select_dtypes(include='number').columns
results_list = []
for col in columns:
current_factor = df[['fecha', col]].dropna()
y = current_factor[col]
x = sm.add_constant(range(len(y)))
model = sm.OLS(y, x)
results = model.fit()
tendencia = round(results.params.iloc[1], 3)
valorP = round(results.pvalues.iloc[1], 3)
results_list.append({
'Factor': col,
'Tendencia': tendencia,
'Valor p': valorP,
'Hay_tendencia': valorP < 0.05
})
fig, ax = plt.subplots(figsize=(20,3))
ax.plot(current_factor.index, current_factor[col], label=col, linewidth=2)
ax.plot(current_factor.index, results.fittedvalues, color='red', linewidth=2)
ax.set_xlabel('Data')
ax.set_ylabel('Valor del factor')
ax.set_title(f'{col}. Tendencia: {tendencia}, Valor p: {valorP}', fontsize=16)
ax.set_xticks([])
plt.show()
pd.DataFrame(results_list)
| Factor | Tendencia | Valor p | Hay_tendencia | |
|---|---|---|---|---|
| 0 | ibex_close | 0.412 | 0.000 | True |
| 1 | deuda_perc_pib | 0.010 | 0.000 | True |
| 2 | pib_trim_per_capita | 0.001 | 0.000 | True |
| 3 | prima | -0.003 | 0.000 | True |
| 4 | tasa_desempleo | 0.000 | 0.021 | True |
Observaciones
Cierre del IBEX-35:
Deuda pública:
PIB trimestral por cápita:
Prima de riesgo:
Tasa de desempleo:
La interpretación de los componentes de la descomposición se verá influenciada por la naturaleza de la serie.
En el caso de una serie aditiva, se entiende que la tendencia, estacionalidad y residuales se juntan de forma aditiva para formar la serie total. Es decir, que los efectos de estos tres componentes se agregan de forma lineal, i el cambio de un componente no afecta al resto.
Si, en cambio, nos encontramos una serie multiplicativa, querrá decir que los componentes se juntan de forma multiplicativa i el cambio de uno puede afectar a la magnitud del otro.
“Para poder saber si el modelo es aditivo o multiplicativo se puede hacer la comparación entre los coeficientes de variación de las series diferencia y el cociente” (Aragon, F., 2017).
Para ello realizaremos lo siguiente:
serie_diferenciada = yt - yt-1
serie_cociente = yt / yt-1
CVD = (serie_diferenciada_σ / serie_diferenciada_μ) * 100
CVC = (serie_cociente_σ / serie_cociente_μ) * 100
Si observamos que CVC es más pequeño que CVD quiere decir que el modelo es multiplicativo. Si por lo contrario, CVC es mayor que CDV, es que el modelo es aditivo.
df['fecha'] = pd.to_datetime(df['fecha'])
numeric_columns = df.select_dtypes(include='number').columns
results_list = []
for col in numeric_columns:
current_factor = df[['fecha', col]].dropna()
diferencia = current_factor[col].diff()
cociente = current_factor[col].pct_change()
diferencia = diferencia.replace([np.inf, -np.inf], np.nan).dropna()
cociente = cociente.replace([np.inf, -np.inf], np.nan).dropna()
cvd = round(current_factor[col].std() / current_factor[col].mean(), 2)
cvd_diferencia = diferencia.std() / diferencia.mean()
cvc = round(cociente.std() / cociente.mean(), 2)
isCVC = cvc > cvd
results_list.append({
'Factor': col,
'CVC': cvc,
'CVD': cvd,
'Aditiva': isCVC
})
pd.DataFrame(results_list)
| Factor | CVC | CVD | Aditiva | |
|---|---|---|---|---|
| 0 | ibex_close | 57.22 | 0.28 | True |
| 1 | deuda_perc_pib | 45.38 | 0.36 | True |
| 2 | pib_trim_per_capita | 24.17 | 0.25 | True |
| 3 | prima | -25.80 | 1.06 | False |
| 4 | tasa_desempleo | -45.28 | 0.32 | False |
Observaciones
Cierre del IBEX-35:
Deuda pública:
PIB trimestral per cápita:
Prima de riesgo:
Tasa de desempleo:
La estacionalidad es un patrón o ciclo que se va repitiendo a intervalos regulares y puede tener una frecuencia estacional durante un año, o en días concretos como los fines de semana. Es decir, la variabilidad que se puede dar en un rango de fechas puede ser anual, mensual, semanal o diaria. Para analizar la estacionalidad de las distintas series se ha usado seasonal_decompose de la librería de statsmodels.
Los resultados de la descomposición estacional se estructuran en:
df['fecha'] = pd.to_datetime(df['fecha'])
numeric_columns = df.select_dtypes(include='number').columns
for col in numeric_columns:
current_factor = df[['fecha', col]].dropna()
seasonal = seasonal_decompose(current_factor[col], model='additive', period=60).plot()
seasonal.set_size_inches((16, 9))
seasonal.tight_layout()
plt.show()
Observaciones
Podemos observar claramente commo no se ve una estacionalidad clara en las series. Esto podría significar que los patrones que emergen al cambiar el periodo podrían estar relacionados con fluctuaciones de largo plazo.
Para complementar los resultados de la descomposición estacional, podemos calcular el Índice de Variancia Estacional (IVE). Este índice es una medida que se usa para cuantificar la magnitud de la variabilidad estacional en una serie temporal.
no_tendencia = $\frac{currentFactorₜ}{\frac{1}{2} \times (\text{currentFactor}_{t-1} + \text{currentFactor}_t)}$
IVE = $\frac{∑_t \text{no_tendencia}_t}{Nₘ}$
Donde:
En el caso que los resultados del IVE sean mayores a 1, podremos entender que hay un componente estacional fuerte en el mes específico. Si el valor está por debajo a 1 querrá decir que la variabilidad estacional disminuye. Por último, si se observa un IVE igual a 1, podremos deducir que el componente estacional no contribuye a la variabilidad de la serie.
title = "IVE de las diferentes series temporales"
centered_title = f"<h1 style='text-align: left;'>{title}</h1>"
display(Markdown(centered_title))
results_IVE = {'Meses': [i for i in range(1,13)]}
df['fecha'] = pd.to_datetime(df['fecha'])
numeric_columns = df.select_dtypes(include='number').columns
for col in numeric_columns:
current_factor = df[['fecha', col]].dropna()
current_factor['mes'] = current_factor['fecha'].dt.month
denominador = (0.5 * (current_factor[col].shift(1) + current_factor[col]))
denominador = denominador.replace(0, pd.NA)
no_tendencia = current_factor[col] / denominador
no_tendencia = no_tendencia.dropna()
IVE = no_tendencia.groupby(current_factor.mes).mean().to_list()
results_IVE[col] = IVE
IVE_df = pd.DataFrame(results_IVE)
display(IVE_df)
| Meses | ibex_close | deuda_perc_pib | pib_trim_per_capita | prima | tasa_desempleo | |
|---|---|---|---|---|---|---|
| 0 | 1 | 1.000118 | 0.999975 | 1.000174 | 0.999449 | 0.999964 |
| 1 | 2 | 1.000130 | 1.000232 | 1.000000 | 1.007056 | 0.999895 |
| 2 | 3 | 0.999930 | 1.000167 | 1.000000 | 0.993269 | 0.999923 |
| 3 | 4 | 1.000455 | 0.999846 | 1.000051 | 1.002283 | 0.999939 |
| 4 | 5 | 0.999848 | 1.000184 | 1.000000 | 0.999384 | 0.999956 |
| 5 | 6 | 0.999772 | 1.000113 | 1.000000 | 1.003537 | 1.000020 |
| 6 | 7 | 1.000056 | 0.999831 | 1.000293 | 0.990718 | 0.999950 |
| 7 | 8 | 0.999752 | 0.999975 | 1.000000 | 1.002841 | 0.999964 |
| 8 | 9 | 0.999847 | 1.000143 | 1.000000 | 1.001847 | 0.999960 |
| 9 | 10 | 1.000307 | 0.999845 | 1.000289 | 1.008203 | 0.999959 |
| 10 | 11 | 1.000496 | 1.000184 | 1.000000 | 1.000134 | 0.999996 |
| 11 | 12 | 1.000245 | 0.999985 | 1.000000 | 0.991976 | 0.999947 |
Observaciones
La mayoría de las series tienen fluctuaciones pequeñas (cercanas a 1), lo que indica que las variaciones estacionales no son muy marcadas.
La correlación serial de una serie nos permite ver si hay relación entre las distintas observaciones de la propia serie a lo largo del tiempo. Dependiendo de esta correlación, podremos ver si los valores pasados tienen algún tipo de influencia en las observaciones del futuro. En este caso, se ha analizado la autocorrelación y la autocorrelación parcial con la librería de statsmodels con las funciones plot_acf y plot_pacf.
ACF
La función de autocorrelación mide la correlación entre el momento t con varias observaciones del pasado $t-1, t-2 … t-n$. Normalmente, cuanto más lejos en el tiempo, menos significancia habrá en los lags.
“Los valores pueden ir de -1 a 1. Un valor próximo a 1 indica una correlación fuerte entre intervalos, y, por lo tanto, los valores del día en cuestión suben siguiendo la tendencia del día anterior. Por el contrario, si el valor es negativo, la correlación es a la inversa. Es decir, los valores de hoy suben cuando los de ayer iban a la baja.” (Villalba, R., 2020).
Los resultados de ACF nos pueden ayudar a identificar el valor de la parte autoregressiva p de los modelos ARIMA o SARIMA.
PACF
En el caso de la autocorrelación parcial, los resultados son los mismos que ACF, pero con la diferencia de que en este caso no se tienen en cuenta los intervalos intermedios. Es decir, $corr(y_t y_{t-2})$ no tendría en cuenta la influencia de $y_{t-1}$. Los resultados de PACF nos ayudan a identificar la media móvil de los modelos ARIMA y SARIMA.
df['fecha'] = pd.to_datetime(df['fecha'])
count = 0
columns = df.select_dtypes(include='number').columns
for col in columns:
current_factor = df[['fecha', col]].dropna()
count += 1
current_factor.set_index('fecha', inplace=True)
fig, (ax1, ax2) = plt.subplots(1, 2,figsize=(16,6), dpi= 80)
acf = sm.graphics.tsa.plot_acf(current_factor, lags=60, ax=ax1)
pacf = sm.graphics.tsa.plot_pacf(current_factor, lags=60, ax=ax2, method='ywm')
fig.text(0.5, 0.95, f"{col}",
fontsize=18, ha='center')
ax1.spines["top"].set_alpha(.3); ax2.spines["top"].set_alpha(.3)
ax1.spines["bottom"].set_alpha(.3); ax2.spines["bottom"].set_alpha(.3)
ax1.spines["right"].set_alpha(.3); ax2.spines["right"].set_alpha(.3)
ax1.spines["left"].set_alpha(.3); ax2.spines["left"].set_alpha(.3)
ax1.tick_params(axis='both', labelsize=12)
ax2.tick_params(axis='both', labelsize=12)
plt.show()
Observaciones
Autocorrelación (ACF)
La autocorrelación muestra una fuerte correlación cercana a 1 en los primeros 20 lags y luego empieza a disminuir levemente en el caso del IBEX-35. En el caso de las demás series, la correlación se extiende por más lags, lo que sugiere patrones de dependencia temporal más prolongados. Para el IBEX-35, este comportamiento indica que los valores de los primeros días están fuertemente relacionados entre sí, lo que sugiere una tendencia persistente en el corto plazo. Esto implica que los movimientos de los primeros días se reflejan fuertemente en los días siguientes. Este tipo de comportamiento es común en series financieras, donde las fluctuaciones a corto plazo suelen estar autocorrelacionadas.
Por otro lado, las otras series presentan una autocorrelación más persistente a lo largo del tiempo, lo que sugiere que tienen una dependencia temporal más duradera.
Autocorrelación Parcial (PACF)
En la autocorrelación parcial (PACF), los primeros dos lags muestran una correlación de 1, mientras que el resto de los lags se acercan a 0. Esto indica que, para todas las series analizadas, los primeros dos periodos tienen una fuerte correlación, pero después la correlación se desvanece rápidamente. Este patrón sugiere que las series podrían seguir un proceso de tipo AR(1) o AR(2), es decir, los valores en el tiempo actual dependen principalmente de los dos valores previos, pero no muestran dependencia significativa después de esos dos lags. Este comportamiento es consistente con series temporales que tienen memoria a corto plazo, en las que las observaciones recientes influyen de forma importante en las futuras, pero no más allá de esos primeros lags.
Este comportamiento es común en datos económicos y financieros, donde las fluctuaciones de corto plazo suelen tener un impacto más inmediato, pero una vez transcurridos unos pocos periodos, la influencia de los valores previos se vuelve mínima.
La estacionariedad en una serie significa que las propiedades estadísticas como la media, la varianza, y la autocorrelación se mantienen constantes a lo largo del tiempo. Si estas propiedades no muestran constancia en el tiempo, la serie se considera no estacionaria. Por el contrario, si estas propiedades son estables, concluiremos que la serie es estacionaria.
Una serie no estacionaria puede mostrar una tendencia, ya sea positiva o negativa, lo que provoca que la media no sea constante en el tiempo. Además, en muchos casos, la varianza también puede cambiar, afectando la estabilidad de la serie.
En una serie estacionaria, la media y la varianza permanecen estables en el tiempo, y no hay evidencia de una tendencia creciente o decreciente. Además, las correlaciones entre observaciones dependen solo de la distancia temporal (lag) entre ellas, no del tiempo absoluto.
Para poder analizar si una serie es estacionaria se ha usado la prueba de Dickey-Fuller (Numxl, 2024) con la librería statsmodels y la función adfuller()
$Δy_t=α+p⋅y_{t-1} +ϵ_t $
Donde:
factores_ADF = []
df['fecha'] = pd.to_datetime(df['fecha'])
numeric_columns = df.select_dtypes(include='number').columns
for col in numeric_columns:
current_factor = df[['fecha', col]].dropna()
result = adfuller(current_factor[col])
new_row = {
'Factor': col,
'EstadisticP_ADF': round(result[0], 2),
'ValorP': round(result[1], 2),
'lags': result[2],
'N_observaciones': result[3],
'5%': round(result[4]['5%'], 2),
'CoefRM': result[5],
'Es_estacionaria': result[1] < 0.05
}
factores_ADF.append(new_row)
factores_ADF_df = pd.DataFrame(factores_ADF)
factores_ADF_df['Es_estacionaria'] = factores_ADF_df['Es_estacionaria'].replace({True: 'Sí', False: 'No'})
factores_ADF_df
| Factor | EstadisticP_ADF | ValorP | lags | N_observaciones | 5% | CoefRM | Es_estacionaria | |
|---|---|---|---|---|---|---|---|---|
| 0 | ibex_close | -2.46 | 0.13 | 5 | 7762 | -2.86 | 96627.666399 | No |
| 1 | deuda_perc_pib | 0.13 | 0.97 | 23 | 7494 | -2.86 | 2014.788835 | No |
| 2 | pib_trim_per_capita | -0.59 | 0.87 | 0 | 7767 | -2.86 | -39009.748185 | No |
| 3 | prima | -1.75 | 0.41 | 35 | 7732 | -2.86 | 49788.408985 | No |
| 4 | tasa_desempleo | -1.36 | 0.60 | 26 | 7741 | -2.86 | -24936.316937 | No |
Observaciones
IBEX-35: El valor p de 0.13 es mayor que el umbral de significancia estándar de 0.05, lo que sugiere que no podemos rechazar la hipótesis nula de que la serie tiene una raíz unitaria, es decir, no es estacionaria. El estadístico ADF de -2.46 es mayor que el umbral de 5% (-2.86), lo que también respalda la conclusión de que la serie no es estacionaria. Dado que la serie del IBEX-35 tiene una tendencia persistente y está influenciada por diversos factores macroeconómicos y políticos, la falta de estacionariedad puede reflejar estos efectos.
Deuda Pública: El valor p de 0.97 es mucho mayor que 0.05, lo que indica que no podemos rechazar la hipótesis nula de no estacionariedad. Por lo tanto, la serie no es estacionaria. El estadístico ADF positivo (0.13) también es una señal de que la serie no tiene una tendencia clara hacia la estacionariedad. La deuda pública tiende a mostrar cambios estructurales y persistentes a lo largo del tiempo debido a políticas fiscales, lo que puede hacer que esta serie sea no estacionaria.
PIB per cápita**: El valor p de 0.87 es mayor que 0.05, por lo que no podemos rechazar la hipótesis nula de no estacionariedad, y la serie no es estacionaria. El estadístico ADF de -0.59 también es mayor que el umbral del 5% (-2.86), lo que respalda que la serie no es estacionaria. El PIB per cápita suele prsentar cambios estructurales a largo plazo debido a cambios en la economía, por lo que es común que esta serie no sea estacionaria.
Prima de riesgo: El valor p de 0.41 es mayor que 0.05, lo que sugiere que no podemos rechazar la hipótesis nula de no estacionariedad. La serie no es estacionaria. El estadístico ADF de -1.75 está por encima de los umbrales del 5% (-2.86), lo que indica que la serie no es estacionaria. La prima de riesgo puede estar influenciada por factores globales y económicos que cambian a lo largo del tiempo, lo que contribuye a la no estacionariedad de la serie.
Tasa de desempleo: El valor p de 0.60 es mayor que 0.05, lo que indica que no podemos rechazar la hipótesis nula de no estacionariedad. Por lo tanto, la serie no es estacionaria. El estadístico ADF de -1.36 es también mayor que el umbral de 5% (-2.86), lo que refuerza la conclusión de que la serie no es estacionaria. La tasa de desempleo es una variable económica que puede verse afectada por políticas macroeconómicas, ciclos económicos y otros factores, lo que puede hacer que la serie sea no estacionaria debido a su comportamiento cambiante a lo largo del tiempo.
La presencia de heterocedasticidad en las series temporales puede perjudicar la capacidad de predicción de los modelos, ya que implica que la variabilidad de los errores no es constante a lo largo del tiempo. Esto puede llevar a estimaciones ineficientes de los parámetros y a predicciones menos precisas. Por esta razón, se ha analizado si las series presentan heterocedasticidad.
Es importante entender que la heterocedasticidad significa que la variabilidad de los errores (o residuos) no es constante. En otras palabras, la dispersión de los valores de la serie puede crecer o decrecer en diferentes partes del espacio temporal, lo que indica que la varianza depende de la posición en la serie. Esto contrasta con la homocedasticidad, en la que la variabilidad se mantiene estable.
En el caso de una serie heterocedástica, la varianza inestable provoca que en ciertas secciones de la serie los errores sean mayores, lo que podría influir negativamente en el ajuste del modelo. Para detectar la heterocedasticidad en las series, se han utilizado las pruebas de Breusch-Pagan y de White, implementadas con la librería statsmodels a través de las funciones het_breushpagan y het_white.
factorsBP = []
count = 0
df['fecha'] = pd.to_datetime(df['fecha'])
numeric_columns = df.select_dtypes(include='number').columns
for col in numeric_columns:
current_factor = df[['fecha', col]].dropna()
count += 1
modelo = sm.OLS(current_factor[col], sm.add_constant(range(len(current_factor))))
resultado_bp = sm.stats.diagnostic.het_breuschpagan(modelo.fit().resid, modelo.fit().model.exog)
resultado_white = sm.stats.diagnostic.het_white(modelo.fit().resid, modelo.fit().model.exog)
new_row = {
'Factor': col,
'EstadisticP_Pagan': round(resultado_bp[0], 2),
'ValorP_Pagan': round(resultado_bp[1], 2),
'EstadisticP_Ajustado': resultado_bp[2],
'ValorP_Ajustado': resultado_bp[3],
'Resultado_Pagan': int(round(resultado_bp[1], 2) < 0.05),
'EstadisticP_White': round(resultado_white[0], 2),
'ValorP_White': round(resultado_white[1], 2),
'Resultado_White': int(round(resultado_white[1], 2) < 0.05)
}
factorsBP.append(new_row)
factorsBP_df = pd.DataFrame(factorsBP)
factorsBP_df.Resultado_Pagan = factorsBP_df['Resultado_Pagan'].replace({0: "No", 1: "Sí"})
factorsBP_df.Resultado_White = factorsBP_df['Resultado_White'].replace({0: "No", 1: "Sí"})
factorsBP_df
| Factor | EstadisticP_Pagan | ValorP_Pagan | EstadisticP_Ajustado | ValorP_Ajustado | Resultado_Pagan | EstadisticP_White | ValorP_White | Resultado_White | |
|---|---|---|---|---|---|---|---|---|---|
| 0 | ibex_close | 875.97 | 0.0 | 987.055657 | 4.577962e-204 | Sí | 886.44 | 0.0 | Sí |
| 1 | deuda_perc_pib | 604.19 | 0.0 | 656.810447 | 5.986956e-139 | Sí | 1274.16 | 0.0 | Sí |
| 2 | pib_trim_per_capita | 20.23 | 0.0 | 20.281638 | 6.781519e-06 | Sí | 70.54 | 0.0 | Sí |
| 3 | prima | 735.26 | 0.0 | 811.925407 | 6.044240e-170 | Sí | 742.63 | 0.0 | Sí |
| 4 | tasa_desempleo | 27.61 | 0.0 | 27.702523 | 1.452631e-07 | Sí | 1254.73 | 0.0 | Sí |
Observaciones
Breusch-Pagan
Esta prueba evalúa si los errores en un modelo de regresión presentan varianza constante (homocedasticidad). Vemos que todos los factores presentan valores extremadamente bajos de 𝑝, lo que permite rechazar la hipótesis nula de homocedasticidad con un nivel de significancia estándar (5%). Por lo tanto, todos los factores son heterocedásticos según esta prueba, indicando que la varianza de los errores no es constante.
White
La prueba de White evalúa heterocedasticidad considerando posibles efectos de interacciones o términos no lineales. En este caso vemos también como todos los factores muestran valores de 𝑝 iguales a 0, indicando una fuerte evidencia contra la hipótesis nula de homocedasticidad. Por lo tanto, todos los factores también son heterocedásticos según esta prueba. Esto refuerza los hallazgos de Breusch-Pagan, ya que se confirma la no estabilidad de la varianza en los residuos.
Por último, analizaremos las posibles anomalías que hay en nuestros datos. Para ello, usaremos IsolationForest, una técnica basada en el aislamiento de observaciones. De esta manera podremos detectar donde se encuentran los valores atípicos.
count = 0
df['fecha'] = pd.to_datetime(df['fecha'])
columns = df.select_dtypes(include='number').columns
for col in columns:
current_factor = df[['fecha', col]].dropna()
current_factor.set_index('fecha', inplace=True)
count += 1
outliers_fraction = float(.01)
scaler = StandardScaler()
np_scaled = scaler.fit_transform(current_factor[col].values.reshape(-1, 1))
data = pd.DataFrame(np_scaled, index=current_factor.index)
model = IsolationForest(contamination=outliers_fraction, random_state=42)
model.fit(data)
current_factor['anomaly'] = model.predict(data)
anomalies = current_factor.loc[current_factor['anomaly'] == -1, [col]]
fig, ax = plt.subplots(figsize=(20, 5))
ax.plot(current_factor.index, current_factor[col], color='black', label='Normal')
ax.scatter(anomalies.index, anomalies[col], color='red', label='Anomalías', zorder=5)
ax.set_title(f'Anomalías en {col}', fontsize=16)
ax.set_xlabel("Fecha", fontsize=14)
ax.set_ylabel(col, fontsize=14)
plt.legend(fontsize=12)
ax.xaxis.set_major_locator(mdates.MonthLocator(interval=12))
ax.xaxis.set_major_formatter(mdates.DateFormatter('%Y-%m'))
plt.xticks(rotation=45, fontsize=12)
plt.yticks(fontsize=12)
plt.grid(axis='x', linestyle='--', alpha=0.7)
plt.show()
Observaciones
Cierre del IBEX-35: En la serie del Cierre del IBEX-35, se identificaron valores atípicos entre los años 2006 y 2007. Este periodo coincide con eventos significativos en los mercados financieros, como las crisis subprime que afectaron la economía global. La volatilidad de los mercados y las caídas repentinas de los índices bursátiles en este periodo provocaron fluctuaciones inusuales en el valor del IBEX-35. Los valores atípicos podrían reflejar reacciones extremas de los inversionistas ante la incertidumbre económica.
Deuda pública: En la serie de Deuda pública, se detectó un valor atípico en 2020, justo cuando se produjo la crisis del COVID-19. La pandemia provocó un aumento significativo en la deuda pública de muchos países debido a los estímulos fiscales, los paquetes de ayuda y los programas de emergencia para hacer frente a los efectos económicos de la crisis sanitaria. Este valor atípico podría reflejar una anomalía en el crecimiento de la deuda durante ese año debido a los gastos extraordinarios y las medidas excepcionales adoptadas por los gobiernos.
PIB trimestral por cápita: En la serie del PIB trimestral por cápita, se observan dos valores atípicos en 2023. Estos podrían estar relacionados con fluctuaciones anómalas en la producción económica, posiblemente debido a cambios estructurales en la economía o eventos macroeconómicos imprevistos. En 2023, algunas economías experimentaron desafíos por el ajuste a la pospandemia, lo que podría haber causado una desviación temporal de los niveles esperados de PIB per cápita. Los factores políticos, la inflación o las tensiones geopolíticas podrían haber influido en la variabilidad observada.
Prima de riesgo: La serie de Prima de riesgo muestra valores atípicos en dos momentos clave: 1995 y 2012. En 1995, la prima de riesgo de varios países europeos experimentó picos debido a la crisis de la deuda en varios países emergentes, que afectaron a la estabilidad financiera global. En 2012, durante la crisis de la zona euro, varios países de la eurozona, como España e Italia, enfrentaron aumentos significativos en sus primas de riesgo debido a la incertidumbre sobre la sostenibilidad de la deuda soberana y la estabilidad de la moneda única europea. Estos valores atípicos reflejan momentos de elevada tensión económica y política en Europa.
Tasa de desempleo: La serie de la Tasa de desempleo presenta valores atípicos en 2007 y 2012. En 2007, el valor atípico podría estar relacionado con los cambios estructurales en el mercado laboral antes de la crisis financiera global de 2008, que comenzó a afectar a muchos países en 2008, especialmente en el mercado laboral. El valor atípico en 2012 se relaciona con los efectos prolongados de la crisis económica global, particularmente en los países de la eurozona, donde las tasas de desempleo se dispararon debido a la recesión económica y las políticas de austeridad. Estos picos reflejan momentos de gran tensión en los mercados laborales y la economía global.
El concept drift ocurre cuando las relaciones estadísticas entre las características de entrada y las etiquetas objetivo cambian con el tiempo, produciendo un problema en el modelo. Este fenómeno es común en sistemas en producción donde los datos evolucionan y presentan una alta volatilidad como es el caso de las finanzas.
La monitorización del concept drift puede tener mucho sentido cuando se hace el aprendizaje por lotes. Estos modelos se entrenan en un conjunto de datos histórico fijo, con lo que el modelo puede quedar atrapado en patrones históricos y pierde precisión cuando los nuevos datos presentan un patrón completamente diferente. En el caso de que esto ocurra, será necesario recopilar nuevos datos y reentrenar el modelo completo, lo que puede ser costoso en tiempo y recursos. En el caso de los modelos en línea, el concept drift no presenta tanto un problema, debido a que estos modelos se entrenan y actualizan continuamente con cada nueva muestra de datos. En nuestro trabajo, los modelos se entrenan cada día, con los nuevos datos que se han registrado en el IBEX-35. Esta capacidad de aprendizaje incremental les permite adaptarse más rápidamente al concept drift, ya que pueden integrar información más reciente en el modelo. Pero, esto no significa que el problema esté completamente resuelto. Depende del concept drift, la adaptación puede ser lenta. Por ejemplo, si tenemos un drift súbito, pueden surgir problemas como la pérdida de patrones importantes del pasado. Es por ello, que monitorizar el concept drift en nuestro aprendizaje en línea puede beneficiar todavía más las predicciones. En este caso, se han usado dos métodos para detectar el concept drift: ADWIN y KSWIN.
def plot_data(stream, drifts=None):
"""
Visualizes a time series and its distribution, highlighting detected concept drifts.
Parameters:
stream : array-like
The time series data to visualize, provided as a list or array of numerical values.
drifts : array-like, optional
A list of indices where concept drifts were detected. If not provided, no drifts are marked.
Returns
None
Displays a plot with the time series and distribution, and marks drift points if any.
"""
fig = plt.figure(figsize=(7, 3), tight_layout=True)
gs = gridspec.GridSpec(1, 2, width_ratios=[3, 1])
ax1, ax2 = plt.subplot(gs[0]), plt.subplot(gs[1])
ax1.grid()
ax1.plot(stream, label='Stream', color='blue')
ax1.set_title('Data Stream')
ax1.set_xlabel('Index')
ax1.set_ylabel('Normalized Values')
ax2.grid(axis='y')
ax2.hist(stream, bins=30, color='gray', alpha=0.7, label='Data Distribution')
ax2.set_title('Distribution')
if drifts is not None:
for drift_detected in drifts:
ax1.axvline(drift_detected, color='red', linestyle='--', label='Drift Detected')
plt.legend()
plt.show()
Adaptative Windowing (ADWIN, por sus siglas en inglés) es un algoritmo diseñado para detectar el concept drift en flujos de datos de manera eficiente. El objetivo principal es mantener el modelo actualizado eliminando automáticamente datos antiguos que ya no representan correctamente la distribución actual. “ADWIN mantiene eficientemente una ventana de longitud variable de elementos recientes, de modo que se sostiene que no ha habido cambios en la distribución de datos. Esta ventana se divide en dos subventanas que se utilizan para determinar si se ha producido un cambio.” (River, 2023). Este cambio se basa en test de hipótesis estadísticos donde primero se calcula la diferencia entre las medias de las dos subventanas y luego se evalúa si esta diferencia excede un umbral estadísticamente significativo, considerando el error estándar. En caso de que se interprete el drift los datos más antiguos se eliminan para así poder dejar la parte más relevante del flujo.
stream = df['ibex_close'].values
stream = (stream - stream.mean()) / stream.std()
drift_detector = drift.ADWIN()
drifts = []
for i, val in enumerate(stream):
drift_detector.update(val)
if drift_detector.drift_detected:
drifts.append(i)
plot_data(stream, drifts)
Observaciones
Se observa que el modelo identifica varias zonas donde se producen cambios significativos en la distribución de los datos a lo largo del tiempo.
El concept drift es un fenómeno en el que la distribución de los datos cambia con el tiempo, lo que significa que el comportamiento o las relaciones en los datos dejan de ser estables, afectando la efectividad de los modelos predictivos. Esto es común en contextos financieros, donde factores externos, eventos inesperados o cambios económicos pueden modificar significativamente el comportamiento de una serie temporal.
En este caso, el algoritmo ADWIN ha detectado múltiples puntos de drift en el cierre del IBEX-35, los cuales están representados en el gráfico por líneas rojas discontinuas. Estos puntos indican que, en esos momentos, la distribución de los valores del índice ha cambiado de forma significativa. Esto es importante, ya que refleja que las dinámicas del mercado, tal vez influenciadas por eventos económicos, políticos o sociales, pueden estar afectando el comportamiento de los valores de la bolsa.
El método Kolmogorov-Smirnov Windowing (KSWIN, por sus siglas en inglés) utiliza el test de Kolmogorov-Smirnov para detectar cambios conceptuales en flujos de datos unidimensionales. KSWIN detecta el concept drift “si la diferencia en las distribuciones de datos empíricos entre las ventanas es demasiado grande, ya que provienen de la misma distribución.” (River, 2023). Para usar este método se ha usado KSWIN de la librería River. A continuación, presentamos la configuración de los parámetros que se ha usado:
data_stream = df['ibex_close'].values
kswin = drift.KSWIN(seed=42)
drifts = []
for i, val in enumerate(data_stream):
_ = kswin.update(val)
if kswin.drift_detected:
drifts.append(i)
plot_data(data_stream, drifts)
Observaciones
Al aplicar el algoritmo KSWIN (a diferencia de ADWIN), se ha detectado una cantidad significativamente mayor de concept drifts en la serie temporal del cierre del IBEX-35. KSWIN es un algoritmo de detección de concept drift más sensible, y su capacidad para detectar múltiples drifts a lo largo del tiempo refleja la alta variabilidad y los posibles cambios abruptos en la dinámica del mercado.